All files / src middleware.ts

71.42% Statements 55/77
63.23% Branches 43/68
100% Functions 7/7
71.83% Lines 51/71

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 1941x   1x     1x                                 1x                     25x         3x 3x                           3x   3x 3x 3x   3x 3x                         3x 2x     2x 2x   2x 1x 1x                   2x   1x       1x           5x 5x   5x         5x         5x               5x       5x   2x 1x 1x 1x 1x     1x       3x   3x   3x   1x 1x 1x       2x 2x   1x 1x 1x       1x   1x 1x                               1x                      
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { ROUTES, STORAGE_KEYS } from '@/constants';
 
// Define protected routes and their required roles
const PROTECTED_ROUTES = {
  [ROUTES.ADMIN.BASE]: ['admin'],
  [ROUTES.ADMIN.USERS]: ['admin'],
  [ROUTES.ADMIN.CONTENT]: ['admin'],
  [ROUTES.ADMIN.CATEGORIES]: ['admin'],
  [ROUTES.ADMIN.SERIES]: ['admin'],
  [ROUTES.ADMIN.ANALYTICS]: ['admin'],
  [ROUTES.RESELLER.BASE]: ['admin', 'reseller'],
  [ROUTES.RESELLER.CUSTOMERS]: ['admin', 'reseller'],
  [ROUTES.RESELLER.CONTENT]: ['admin', 'reseller'],
  [ROUTES.RESELLER.ANALYTICS]: ['admin', 'reseller'],
  [ROUTES.USER.BASE]: ['admin', 'reseller', 'end_user'],
  [ROUTES.USER.CONTENT]: ['admin', 'reseller', 'end_user'],
  [ROUTES.USER.DEVICES]: ['admin', 'reseller', 'end_user'],
  [ROUTES.USER.PROFILE]: ['admin', 'reseller', 'end_user']} as const;
 
// Public routes that don't require authentication
const PUBLIC_ROUTES = [
  ROUTES.HOME,
  ROUTES.LOGIN,
  ROUTES.DEMO,
  ROUTES.REDEEM,
  '/banned', // Allow access to banned page for banned users
  '/forgot-password',
  '/reset-password',
];
 
function isPublicRoute(pathname: string): boolean {
  return PUBLIC_ROUTES.some((route) => pathname === route || pathname.startsWith(`${route}/`));
}
 
function getRequiredRoles(pathname: string): string[] | null {
  // Check exact match first
  Eif (PROTECTED_ROUTES[pathname as keyof typeof PROTECTED_ROUTES]) {
    return [...PROTECTED_ROUTES[pathname as keyof typeof PROTECTED_ROUTES]];
  }
 
  // Check if pathname starts with any protected route
  for (const [route, roles] of Object.entries(PROTECTED_ROUTES)) {
    if (pathname.startsWith(route)) {
      return [...roles];
    }
  }
 
  return null;
}
 
function getUserFromToken(token: string): { role: string } | null {
  try {
    // Decode JWT token payload (works both in edge and node runtimes)
    const base = token.split('.')[1] || '';
    const payloadJson = (() => {
      try {
        // prefer atob if available (edge/browser-like runtimes)
        Eif (typeof atob === 'function') {
          return atob(base);
        }
      } catch {
        // fallthrough
      }
      // Node fallback
      try {
        return Buffer.from(base, 'base64').toString('utf-8');
      } catch {
        return '';
      }
    })();
 
    if (!payloadJson) return null;
    const payload = JSON.parse(payloadJson || '{}');
 
    // Normalize role strings into canonical values used by the app
    const rawRole = String(payload.role ?? '').trim();
    const normalized = rawRole.toLowerCase().replace(/[-\s]/g, '_');
 
    if (normalized === 'admin') return { role: 'admin' };
    Iif (normalized === 'reseller') return { role: 'reseller' };
    Eif (normalized === 'end_user' || normalized === 'enduser' || normalized.includes('user')) return { role: 'end_user' };
 
    // Fallback: return normalized value
    return { role: normalized };
  } catch {
    return null;
  }
}
 
function getDefaultRouteForRole(role: string): string {
  switch (role) {
    case 'admin':
      return ROUTES.ADMIN.BASE;
    case 'reseller':
      return ROUTES.RESELLER.BASE;
    case 'end_user':
      return ROUTES.USER.BASE;
    default:
      return ROUTES.HOME;
  }
}
 
export function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl;
 
  Iif (pathname === ROUTES.DEMO || pathname.startsWith(`${ROUTES.DEMO}/`) || pathname === ROUTES.REDEEM || pathname.startsWith(`${ROUTES.REDEEM}/`)) {
    return NextResponse.next();
  }
 
  // Skip middleware entirely for static builds
  Iif (process.env.NODE_ENV === 'production' && process.env.NEXT_STATIC_EXPORT === 'true') {
    return NextResponse.next();
  }
 
  // Skip middleware for static files and API routes
  Iif (
    pathname.startsWith('/_next/') ||
    pathname.startsWith('/api/') ||
    pathname.includes('.') // Static files
  ) {
    return NextResponse.next();
  }
  // Get token from cookies or headers
  const token = request.cookies.get(STORAGE_KEYS.AUTH_TOKEN)?.value ||
                request.headers.get('authorization')?.replace('Bearer ', '');
 
  // Check if route is public
  if (isPublicRoute(pathname)) {
    // If user is authenticated and trying to access login, redirect to dashboard
    if (token && pathname === ROUTES.LOGIN) {
      const user = getUserFromToken(token);
      Eif (user) {
        const defaultRoute = getDefaultRouteForRole(user.role);
        return NextResponse.redirect(new URL(defaultRoute, request.url));
      }
    }
    return NextResponse.next();
  }
 
  // Check if route requires authentication
  const requiredRoles = getRequiredRoles(pathname);
  
  Eif (requiredRoles) {
    // Route requires authentication
    if (!token) {
      // No token, redirect to login
      const loginUrl = new URL(ROUTES.LOGIN, request.url);
      loginUrl.searchParams.set('redirect', pathname);
      return NextResponse.redirect(loginUrl);
    }
 
    // Verify token and check role
    const user = getUserFromToken(token);
    if (!user) {
      // Invalid token, redirect to login
      const loginUrl = new URL(ROUTES.LOGIN, request.url);
      loginUrl.searchParams.set('redirect', pathname);
      return NextResponse.redirect(loginUrl);
    }
 
    // Check if user has required role
    Eif (!requiredRoles.includes(user.role)) {
      // User doesn't have required role, redirect to their default dashboard
      const defaultRoute = getDefaultRouteForRole(user.role);
      return NextResponse.redirect(new URL(defaultRoute, request.url));
    }
  }
 
  // Handle root path redirect for authenticated users
  if (pathname === ROUTES.HOME && token) {
    const user = getUserFromToken(token);
    if (user) {
      const defaultRoute = getDefaultRouteForRole(user.role);
      return NextResponse.redirect(new URL(defaultRoute, request.url));
    }
  }
 
  return NextResponse.next();
}
 
export const config = {
  matcher: [
    /*
     * Match all request paths except for the ones starting with:
     * - api (API routes)
     * - _next/static (static files)
     * - _next/image (image optimization files)
     * - favicon.ico (favicon file)
     */
    '/((?!api|_next/static|_next/image|favicon.ico).*)',
  ]};